// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import '../config/config.dart';
import '../elements/elements.dart';
import '../logging/logging.dart';
import '../util/find_package.dart';
import '../util/string_util.dart';
import 'visitor.dart';

class CFieldName extends Visitor<Field, String> {
  const CFieldName();

  @override
  String visit(Field node) {
    final className = node.classDecl.uniqueName;
    final fieldName = node.finalName;
    return '${className}__$fieldName';
  }
}

class CMethodName extends Visitor<Method, String> {
  const CMethodName();

  @override
  String visit(Method node) {
    final className = node.classDecl.uniqueName;
    final methodName = node.finalName;
    return '${className}__$methodName';
  }
}

class CGenerator extends Visitor<Classes, Future<void>> {
  static const _prelude = '''// Autogenerated by jnigen. DO NOT EDIT!

#include <stdint.h>
#include "jni.h"
#include "dartjni.h"

thread_local JNIEnv *jniEnv;
JniContext *jni;

JniContext *(*context_getter)(void);
JNIEnv *(*env_getter)(void);

void setJniGetters(JniContext *(*cg)(void),
        JNIEnv *(*eg)(void)) {
    context_getter = cg;
    env_getter = eg;
}

''';

  final Config config;

  CGenerator(this.config);

  Future<void> _copyFileFromPackage(String package, String relPath, Uri target,
      {String Function(String)? transform}) async {
    final packagePath = await findPackageRoot(package);
    if (packagePath != null) {
      final sourceFile = File.fromUri(packagePath.resolve(relPath));
      final targetFile = await File.fromUri(target).create(recursive: true);
      var source = await sourceFile.readAsString();
      if (transform != null) {
        source = transform(source);
      }
      await targetFile.writeAsString(source);
    } else {
      log.warning('package $package not found! '
          'skipped copying ${target.toFilePath()}');
    }
  }

  @override
  Future<void> visit(Classes node) async {
    // Write C file and init file.
    final cConfig = config.outputConfig.cConfig!;
    final cRoot = cConfig.path;
    final preamble = config.preamble;
    log.info("Using c root = $cRoot");
    final libraryName = cConfig.libraryName;
    log.info('Creating dart init file ...');
    // Create C file.
    final subdir = cConfig.subdir ?? '.';
    final cFileRelativePath = '$subdir/$libraryName.c';
    final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath))
        .create(recursive: true);
    final cFileStream = cFile.openWrite();
    // Write C Bindings.
    if (preamble != null) {
      cFileStream.writeln(preamble);
    }
    cFileStream.write(_prelude);
    final classGenerator = _CClassGenerator(config, cFileStream);
    for (final classDecl in node.decls.values) {
      classDecl.accept(classGenerator);
    }
    await cFileStream.close();
    log.info('Copying auxiliary files...');
    for (final file in ['dartjni.h', '.clang-format']) {
      await _copyFileFromPackage(
          'jni', 'src/$file', cRoot.resolve('$subdir/$file'));
    }
    await _copyFileFromPackage(
        'jnigen', 'cmake/CMakeLists.txt.tmpl', cRoot.resolve('CMakeLists.txt'),
        transform: (s) {
      return s
          .replaceAll('{{LIBRARY_NAME}}', libraryName)
          .replaceAll('{{SUBDIR}}', subdir);
    });
    log.info('Running clang-format on C bindings');
    try {
      final clangFormat = Process.runSync('clang-format', ['-i', cFile.path]);
      if (clangFormat.exitCode != 0) {
        printError(clangFormat.stderr);
        log.warning('clang-format exited with ${clangFormat.exitCode}');
      }
    } on ProcessException catch (e) {
      log.warning('cannot run clang-format: $e');
    }
  }
}

const _classVarPrefix = '_c_';
const _jniResultType = 'JniResult';
const _loadEnvCall = 'load_env();';
const _ifError =
    '(JniResult){.value = {.j = 0}, .exception = check_exception()}';

class _CClassGenerator extends Visitor<ClassDecl, void> {
  final Config config;
  final StringSink s;

  _CClassGenerator(this.config, this.s);

  @override
  void visit(ClassDecl node) {
    final classNameInC = node.uniqueName;
    final classVar = '$_classVarPrefix$classNameInC';
    // Global variable in C that holds the reference to class.
    s.write('''// ${node.binaryName}
jclass $classVar = NULL;

''');

    final methodGenerator = _CMethodGenerator(config, s);
    for (final method in node.methods) {
      method.accept(methodGenerator);
    }

    final fieldGenerator = _CFieldGenerator(config, s);
    for (final field in node.fields) {
      field.accept(fieldGenerator);
    }
  }
}

class _CLoadClassGenerator extends Visitor<ClassDecl, String> {
  _CLoadClassGenerator();

  @override
  String visit(ClassDecl node) {
    final classVar = '$_classVarPrefix${node.uniqueName}';
    return '''    load_class_global_ref(&$classVar, "${node.internalName}");
    if ($classVar == NULL) return $_ifError;''';
  }
}

class _CMethodGenerator extends Visitor<Method, void> {
  static const _methodVarPrefix = '_m_';

  final Config config;
  final StringSink s;

  _CMethodGenerator(this.config, this.s);

  @override
  void visit(Method node) {
    final classNameInC = node.classDecl.uniqueName;

    final cMethodName = node.accept(const CMethodName());
    final classRef = '$_classVarPrefix$classNameInC';
    final methodId = '$_methodVarPrefix$cMethodName';
    final cMethodParams = [
      if (!node.isCtor && !node.isStatic) 'jobject self_',
      ...node.params.accept(const _CParamGenerator(addReturnType: true)),
    ].join(',');
    final jniSignature = node.descriptor;
    final ifStaticMethodID = node.isStatic ? 'static_' : '';

    var javaReturnType = node.returnType.type;
    if (node.isCtor) {
      javaReturnType = DeclaredType(
        binaryName: node.classDecl.binaryName,
      );
    }
    final callType = node.returnType.accept(const _CTypeCallSite());
    final callArgs = [
      'jniEnv',
      if (!node.isCtor && !node.isStatic) 'self_' else classRef,
      methodId,
      ...node.params.accept(const _CParamGenerator(addReturnType: false))
    ].join(', ');

    var ifAssignResult = '';
    if (javaReturnType.name != 'void') {
      ifAssignResult =
          '${javaReturnType.accept(const _CReturnType())} _result = ';
    }

    final ifStaticCall = node.isStatic ? 'Static' : '';
    final envMethod =
        node.isCtor ? 'NewObject' : 'Call$ifStaticCall${callType}Method';
    final returnResultIfAny = javaReturnType.accept(const _CResult());
    s.write('''
jmethodID $methodId = NULL;
FFI_PLUGIN_EXPORT
$_jniResultType $cMethodName($cMethodParams) {
    $_loadEnvCall
    ${node.classDecl.accept(_CLoadClassGenerator())}
    load_${ifStaticMethodID}method($classRef,
      &$methodId, "${node.name}", "$jniSignature");
    if ($methodId == NULL) return $_ifError;
    $ifAssignResult(*jniEnv)->$envMethod($callArgs);
    $returnResultIfAny
}

''');
  }
}

class _CFieldGenerator extends Visitor<Field, void> {
  static const _fieldVarPrefix = '_f_';

  final Config config;
  final StringSink s;

  _CFieldGenerator(this.config, this.s);

  @override
  void visit(Field node) {
    final cClassName = node.classDecl.uniqueName;

    final fieldName = node.finalName;
    final fieldNameInC = node.accept(const CFieldName());
    final fieldVar = "$_fieldVarPrefix$fieldNameInC";

    // If the field is final and default is assigned, then no need to wrap
    // this field. It should then be a constant in dart code.
    if (node.isStatic &&
        node.isFinal &&
        node.defaultValue != null &&
        (node.defaultValue is num || node.defaultValue is bool)) {
      return;
    }

    s.write('jfieldID $fieldVar = NULL;\n');

    final classVar = '$_classVarPrefix$cClassName';
    void writeAccessor({bool isSetter = false}) {
      const cReturnType = _jniResultType;
      final cMethodPrefix = isSetter ? 'set' : 'get';
      final formalArgs = [
        if (!node.isStatic) 'jobject self_',
        if (isSetter) '${node.type.accept(const _CReturnType())} value',
      ].join(', ');
      final ifStaticField = node.isStatic ? 'static_' : '';
      final ifStaticCall = node.isStatic ? 'Static' : '';
      final callType = node.type.accept(const _CTypeCallSite());
      final objectArgument = node.isStatic ? classVar : 'self_';

      String accessorStatements;
      if (isSetter) {
        accessorStatements =
            '    (*jniEnv)->Set$ifStaticCall${callType}Field(jniEnv, '
            '$objectArgument, $fieldVar, value);\n'
            '    return $_ifError;';
      } else {
        final getterExpr =
            '(*jniEnv)->Get$ifStaticCall${callType}Field(jniEnv, '
            '$objectArgument, $fieldVar)';
        final cResultType = node.type.accept(const _CReturnType());
        final result = node.type.accept(const _CResult());
        accessorStatements = '''    $cResultType _result = $getterExpr;
    $result''';
      }
      s.write('''
FFI_PLUGIN_EXPORT
$cReturnType ${cMethodPrefix}_$fieldNameInC($formalArgs) {
    $_loadEnvCall
    ${node.classDecl.accept(_CLoadClassGenerator())}
    load_${ifStaticField}field($classVar, &$fieldVar, "$fieldName",
      "${node.type.descriptor}");
$accessorStatements
}

''');
    }

    writeAccessor(isSetter: false);
    if (node.isFinal) {
      return;
    }
    writeAccessor(isSetter: true);
  }
}

class _CParamGenerator extends Visitor<Param, String> {
  /// These should be avoided in parameter names.
  static const _cTypeKeywords = {
    'short',
    'char',
    'int',
    'long',
    'float',
    'double',
  };

  const _CParamGenerator({required this.addReturnType});

  final bool addReturnType;

  @override
  String visit(Param node) {
    final paramName =
        (_cTypeKeywords.contains(node.name) ? '${node.name}0' : node.name)
            .replaceAll('\$', '_');
    if (addReturnType) {
      final type = node.type.accept(const _CReturnType());
      return '$type $paramName';
    }
    return paramName;
  }
}

class _CReturnType extends TypeVisitor<String> {
  const _CReturnType();

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return 'jobject';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    return node.cType;
  }
}

class _CTypeCallSite extends TypeVisitor<String> {
  const _CTypeCallSite();

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return 'Object';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    return node.name.capitalize();
  }
}

class _CResult extends TypeVisitor<String> {
  const _CResult();

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return 'return to_global_ref_result(_result);';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'void') {
      return 'return $_ifError;';
    }
    // The union field is the same as the type's signature, but in lowercase.
    final unionField = node.signature.toLowerCase();
    return 'return (JniResult){.value = {.$unionField = _result}, '
        '.exception = check_exception()};';
  }
}
